package com.piece.core.session.filter;

import com.piece.core.framework.constant.HeaderConstants;
import com.piece.core.web.properties.SecurityProperties;
import com.piece.core.session.http.HttpSessionAdapter;
import com.piece.core.session.repository.TokenSessionRepository;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.Order;
import org.springframework.security.web.util.OnCommittedResponseWrapper;
import org.springframework.session.SessionRepository;
import org.springframework.session.web.http.HttpSessionIdResolver;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.Iterator;
import java.util.List;

@Order(Integer.MIN_VALUE)
public class DefaultSessionRepositoryFilter<S extends TokenSessionRepository.RedisSession> extends OncePerRequestFilter {

    private static final String SESSION_LOGGER_NAME = DefaultSessionRepositoryFilter.class.getName().concat(".SESSION_LOGGER");
    private static final Log SESSION_LOGGER;
    public static final String SESSION_REPOSITORY_ATTR;
    public static final String INVALID_SESSION_ID_ATTR;
    private static final String CURRENT_SESSION_ATTR;
    private final TokenSessionRepository sessionRepository;
    private ServletContext servletContext;
    private HttpSessionIdResolver httpSessionIdResolver;
    protected SecurityProperties securityProperties;

    public DefaultSessionRepositoryFilter(TokenSessionRepository sessionRepository) {
        if (null == sessionRepository) {
            throw new IllegalArgumentException("sessionRepository cannot be null");
        } else {
            this.sessionRepository = sessionRepository;
        }
    }

    public void setHttpSessionIdResolver(HttpSessionIdResolver httpSessionIdResolver) {
        if (null == httpSessionIdResolver) {
            throw new IllegalArgumentException("httpSessionIdResolver cannot be null");
        } else {
            this.httpSessionIdResolver = httpSessionIdResolver;
        }
    }

    public void setSecurityProperties(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
        SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response, this.servletContext);
        SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest, response);

        try {
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        } finally {
            wrappedRequest.commitSession();
        }
    }

    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    static {
        SESSION_LOGGER = LogFactory.getLog(SESSION_LOGGER_NAME);
        SESSION_REPOSITORY_ATTR = SessionRepository.class.getName();
        INVALID_SESSION_ID_ATTR = SESSION_REPOSITORY_ATTR + ".invalidSessionId";
        CURRENT_SESSION_ATTR = SESSION_REPOSITORY_ATTR + ".CURRENT_SESSION";
    }

    private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
        private final HttpServletResponse response;
        private final ServletContext servletContext;
        private S requestedSession;
        private boolean requestedSessionCached;
        private String requestedSessionId;
        private Boolean requestedSessionIdValid;
        private boolean requestedSessionInvalidated;

        private SessionRepositoryRequestWrapper(HttpServletRequest request, HttpServletResponse response, ServletContext servletContext) {
            super(request);
            this.response = response;
            this.servletContext = servletContext;
        }

        private void commitSession() {
            HttpSessionWrapper wrappedSession = this.getCurrentSession();
            if (null == wrappedSession) {
                if (this.isInvalidateClientSession()) {
                    DefaultSessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
                }
            } else {
                S session = wrappedSession.getSession();
                this.clearRequestedSessionCache();
                session.setLastAccessedTime(Instant.now());
                DefaultSessionRepositoryFilter.this.sessionRepository.save(session);
                String sessionId = session.getId();
                if (!this.isRequestedSessionIdValid() || !sessionId.equals(this.getRequestedSessionId())) {
                    DefaultSessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
                }
            }

        }

        private HttpSessionWrapper getCurrentSession() {
            return (HttpSessionWrapper)this.getAttribute(DefaultSessionRepositoryFilter.CURRENT_SESSION_ATTR);
        }

        private void setCurrentSession(HttpSessionWrapper currentSession) {
            if (null == currentSession) {
                this.removeAttribute(DefaultSessionRepositoryFilter.CURRENT_SESSION_ATTR);
            } else {
                this.setAttribute(DefaultSessionRepositoryFilter.CURRENT_SESSION_ATTR, currentSession);
            }

        }

        public String changeSessionId() {
            HttpSession session = this.getSession(false);
            if (null == session) {
                throw new IllegalStateException("Cannot change session ID. There is no session associated with this request.");
            } else {
                return this.getCurrentSession().getSession().changeSessionId();
            }
        }

        public boolean isRequestedSessionIdValid() {
            if (this.requestedSessionIdValid == null) {
                S requestedSession = this.getRequestedSession();
                if (null != requestedSession) {
                    requestedSession.setLastAccessedTime(Instant.now());
                }

                return this.isRequestedSessionIdValid(requestedSession);
            } else {
                return this.requestedSessionIdValid;
            }
        }

        private boolean isRequestedSessionIdValid(S session) {
            if (null == this.requestedSessionIdValid) {
                this.requestedSessionIdValid = (null != session);
            }

            return this.requestedSessionIdValid;
        }

        private boolean isInvalidateClientSession() {
            return (null == this.getCurrentSession()) && this.requestedSessionInvalidated;
        }

        public HttpSessionWrapper getSession(boolean create) {
            HttpSessionWrapper currentSession = this.getCurrentSession();
            HttpServletRequest request = (HttpServletRequest) this.getRequest();
            String token = request.getHeader(HeaderConstants.Authorization);

            S requestedSession = this.getRequestedSession();
            if (null != requestedSession) {
                if (null == this.getAttribute(DefaultSessionRepositoryFilter.INVALID_SESSION_ID_ATTR)) {
                    requestedSession.setLastAccessedTime(Instant.now());
                    requestedSession.setMaxInactiveInterval(Duration.ofMinutes(DefaultSessionRepositoryFilter.this.securityProperties.getSession().getExpire()));
                    this.requestedSessionIdValid = true;
                    currentSession = new HttpSessionWrapper(requestedSession, this.getServletContext());
                    currentSession.setNew(false);
                    this.setCurrentSession(currentSession);
                    return currentSession;
                }
            } else {
                if (DefaultSessionRepositoryFilter.SESSION_LOGGER.isDebugEnabled()) {
                    DefaultSessionRepositoryFilter.SESSION_LOGGER.debug("No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
                }

                this.setAttribute(DefaultSessionRepositoryFilter.INVALID_SESSION_ID_ATTR, "true");
            }

            if (!create) {
                return null;
            } else {
                if (DefaultSessionRepositoryFilter.SESSION_LOGGER.isDebugEnabled()) {
                    DefaultSessionRepositoryFilter.SESSION_LOGGER.debug("A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for " + DefaultSessionRepositoryFilter.SESSION_LOGGER_NAME, new RuntimeException("For debugging purposes only (not an error)"));
                }

                S session = null;
                if (null != token) {
                    session = (S) DefaultSessionRepositoryFilter.this.sessionRepository.createSession(token);
                } else {
                    session = (S) DefaultSessionRepositoryFilter.this.sessionRepository.createSession();
                }

                session.setMaxInactiveInterval(Duration.ofMinutes(DefaultSessionRepositoryFilter.this.securityProperties.getSession().getExpire()));
                session.setLastAccessedTime(Instant.now());
                currentSession = new HttpSessionWrapper(session, this.getServletContext());
                this.setCurrentSession(currentSession);
                return currentSession;
            }
        }

        public ServletContext getServletContext() {
            return (null != this.servletContext) ? this.servletContext : super.getServletContext();
        }

        public HttpSessionWrapper getSession() {
            return this.getSession(true);
        }

        public String getRequestedSessionId() {
            if (null == this.requestedSessionId) {
                this.getRequestedSession();
            }

            return this.requestedSessionId;
        }

        public RequestDispatcher getRequestDispatcher(String path) {
            RequestDispatcher requestDispatcher = super.getRequestDispatcher(path);
            return new SessionCommittingRequestDispatcher(requestDispatcher);
        }

        private S getRequestedSession() {
            if (!this.requestedSessionCached) {
                List<String> sessionIds = DefaultSessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);
                Iterator var2 = sessionIds.iterator();

                while(var2.hasNext()) {
                    String sessionId = (String)var2.next();
                    if (null == this.requestedSessionId) {
                        this.requestedSessionId = sessionId;
                    }

                    S session = (S) DefaultSessionRepositoryFilter.this.sessionRepository.findById(sessionId);
                    if (null != session) {
                        this.requestedSession = session;
                        this.requestedSessionId = sessionId;
                        break;
                    }
                }

                this.requestedSessionCached = true;
            }

            return this.requestedSession;
        }

        private void clearRequestedSessionCache() {
            this.requestedSessionCached = false;
            this.requestedSession = null;
            this.requestedSessionId = null;
        }

        private final class SessionCommittingRequestDispatcher implements RequestDispatcher {
            private final RequestDispatcher delegate;

            SessionCommittingRequestDispatcher(RequestDispatcher delegate) {
                this.delegate = delegate;
            }

            public void forward(ServletRequest request, ServletResponse response) throws ServletException, IOException {
                this.delegate.forward(request, response);
            }

            public void include(ServletRequest request, ServletResponse response) throws ServletException, IOException {
                SessionRepositoryRequestWrapper.this.commitSession();
                this.delegate.include(request, response);
            }
        }

        private final class HttpSessionWrapper extends HttpSessionAdapter<S> {
            HttpSessionWrapper(S session, ServletContext servletContext) {
                super(session, servletContext);
            }

            public void invalidate() {
                super.invalidate();
                SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;
                SessionRepositoryRequestWrapper.this.setCurrentSession(null);
                SessionRepositoryRequestWrapper.this.clearRequestedSessionCache();
                DefaultSessionRepositoryFilter.this.sessionRepository.deleteById(this.getId());
            }
        }
    }

    private final class SessionRepositoryResponseWrapper extends OnCommittedResponseWrapper {
        private final SessionRepositoryRequestWrapper request;

        SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request, HttpServletResponse response) {
            super(response);
            if (null == request) {
                throw new IllegalArgumentException("request cannot be null");
            } else {
                this.request = request;
            }
        }

        protected void onResponseCommitted() {
            this.request.commitSession();
        }
    }
}
